美团外卖前端容器化演进实践
总第372篇
2019年 第50篇
背景
提单页的位置
所支撑的业务
提单页绝大部分模块的需求开发和日常维护都是由外卖侧的研发同学在负责,包括地址模块、商家商品信息模块、折扣信息模块、准时宝、隐私号、发票备注等。
闪购侧业务
当从商超等频道进入提单页时,提单页生成的是闪购侧订单,闪购侧的订单在配送方式、红包、下单路径上都与外卖订单有所区别,但又依赖于外卖的基础功能模块,因此与外卖侧功能存在严重的耦合问题。
其他业务
提单页上的部分模块对动态化配置能力有着很高的要求,这些模块使用Mach等动态化模版来实现相关的业务逻辑,由专门的业务组负责开发和维护。
随着业务的不断迭代,提单页的模块也越来越多,逻辑的耦合也越来越重。现在提单页的UI展示模块已经超过30个,这些模块的展示与否基本上通过服务端的下发数据来决定。在不同的订单类型下,提单页所展示元素的差异越来越大,很多模块的代码已经不适合统一放在一起维护,代码拆分的需求十分强烈。此外,客户端包体积是衡量客户端性能的重要指标,为了解决业务发展带来的提单页代码量急剧增长的问题,同时实现页面元素的动态配置,我们希望能够实现提单页的动态化,而动态化需要基于容器来实现,所以我们提出了提单页的容器方案。
问题和挑战
提单页整体动态化的需求不是很强烈,并且API改造的成本比较高,因此API接口字段保持不变,需要在客户端层面去做转换。
首页模块基本仅作为展示用途,提单页模块的交互逻辑要复杂一些,比如发票模块,进入二级页面操作完成后还要更新提单页的数据。
首页模块的UI展示各模块之间是完全独立的,而提单页的模块是根据功能聚合在一个组,这些模块条件出现的位置不同,展示的样式也不一致,如下图备注发票模块所示,最上层和最底层的模块上都带有圆角,所以提单页需要外层再添加一个模块组。
容器化后的提单页,需要实现模块之间的互相无感知,根据服务端的下发数据,客户端可以将闪购代码仓库内的模块和外卖代码仓库内的模块拼接起来组成完整的提单页展示给用户。当用户在提单页完成一系列操作时,各模块可以提供必要的参数给服务端。要想实现这一点,我们需要考虑以下几个问题:
模块注册问题,如何在无直接依赖的情况下,让提单页获取页面可用模块。
API数据分发问题,如何将服务端字段转换为模块可用数据,同时不侵入到模块这一层。
通信问题,模块之间如何实现联动效果。
页面更新和复用问题,在提单页刷新时如何提交数据给服务端以及如何完成模块的更新。
设计方案
1. 容器化整体的架构图设计
容器化是我们在外卖平台化之后对多方业务能力的支持和扩展,在不改变API数据源等前提下,我们保证其具有动态可配置化的能力。为了更好地支撑业务,我们在业务层面抽离出来容器化框架层,其所提供三个部分的核心功能: 1.功能节点扩展及通信功能;2.可配置化功能;3.数据分发功能。在最上层业务容器中,目前所支持外卖提单页面模块、闪购提单页模块、提单页Mach(外卖动态化模板)模块、提单页MRN(RN页面)模块四种不同的业务。
1.1 概念解释
Block有两种类型:其一是普通的Block,其包含BlockView(视图层)和BlockViewModel(数据层)。BlockView(视图层)用来展示具体的视图以及内部的业务逻辑;BlockViewModel(数据层),用来数据解析。其二是LogicBlock,是没有视图的Block,单纯地用来做数据业务处理。
1.2 整体概述
在容器化之前,我们的业务大多是模块化的结构,模块化宿主类是承载所有模块化的管理类,各个模块之间通过宿主类或者控制器进行数据交互。但在容器化改造中,我们将之前宿主类中管理的模块进行拆解,并重新定义了宿主类的职责。在容器化宿主类中,我们将不再持有各个功能模块的引用,而只要持有Root Block这一个实例,就可以完成对所有功能模块的管理。而Root Block Context则用来处理父Block与子Block之间的通信以及子Block之间的通信。
1.3 核心功能
第一部分功能节点扩展及通信功能。主要是目前页面的集成和通信关系,其中Root Block是Block Tree的根节点,下面会挂载一些SubBlock子节点,Root Block会控制整体的数据流的分发以及整体样式;Root Block Context可以理解为上下文环境或通信的总线。每个模块都有自己的Context,来维护自己向外部提供数据以及业务逻辑的能力,这些子Context会统一注册到Root Context中进行管理维护。
第二部分可配置化功能。在发起数据请求成功之后,客户端根据注册的Key以及接收到的数据,动态创建Block的容器化能力。遍历解析数据以及配置文件,先动态创建viewModel,将创建好的viewModel绑定到生成的Block模块上,动态添加到Root Block中。多业务方在完全不用相互感知的情况下,完成对新增模块的开发。
第三部分数据分发。既将解析之后的数据,由Root Block节点进行数据分发到各个子Block,各子Block的BlockViewModel在更新数据之后并回传到Block中,Block用更新后的数据更新View的展示。其中,数据可以自动完成分发,也可以手动的接管数据流进行相应的处理。
2. Block注册问题
2.1 Android 注册的设计方案
@DynamicBinder(nativeId = "block_key_d", viewModel = blockDViewModel.class, modelType = blockDInfo.class)
NativeID是用来标识Block块的唯一Key,viewModel是用来绑定View视图的数据层, modelType对应着API的数据Model。
2.2 iOS 注册的设计方案
KLN_STRING_EXPORT("AppKey_"
3. API数据结构化
{
"data":{
"xxx_pay_by_friend": true,
"xxx_by_friend_tip": "发给微信好友,让TA买单",
"xxx_by_friend_bubble_desc": "找微信好友买单",
"xxx_friend_order": false
}
"code":0,
"msg":""
}
由于这种格式是平铺分散的,没有将特定功能点的字段聚合在一起表示,不利于我们动态地将数据Model与Block绑定在一起。
{
"data":{
"pay_by_friend":{//key
"xxx_pay_by_friend": true,
"xxx_by_friend_tip": "发给微信朋友,让TA买单",
"xxx_by_friend_bubble_desc": "找微信好友买单",
"xxx_friend_order": false
}
}
"code":0,
"msg":""
}
将平铺的API数据整理成定制的结构化数据,将Key作为唯一的标识,那么就可以方便地用来对应指定模块化Block中所需的数据Model。
{
"layout_info":[
{"native_id":"order_pay_by_friend","data_key":"pay_by_friend"},
{
"native_id":"block_container_default",//容器组
"children":[
{"native_id":"order_flower_cake","data_key":"flower_cake"}
]
}
]
}
当然,这里可以以组为维度将一些功能相似的模块聚合在一起,native_id的含义同上,Children是子Block结点的数组。
4. 模块间通信问题
4.1 Command数据交互方式
//声明事件容器
private SupplierCommand<Object> mSupplierCommand = new SupplierCommand<>();
public SupplierCommand<Object> getSupplierCommand() {
return mSupplierCommand;
}
//注册实现
context().getSupplierCommand().registerCommand(new Supplier() {
public Object run() {
}
});
//获取相应的Object对象
context().getSupplierCommand().execute();
4.2 Event数据交互方式
//声明事件容器
private SupplierEvent mSupplierEvent = new SupplierEvent();
public SupplierEvent supplierResponseEvent() {
return mSupplierEvent;
}
//实现订阅
context().supplierResponseEvent().subscribe(new Action() {
public void action() {
}
});
//触发相应的操作
context().supplierResponseEvent().trigger();
5. Block页面数据分发问题
5.1 数据分发问题
Block Tree数据分发逻辑简介图
5.2 Block创建的顺序
5.3 数据拉取问题
6. Block页面的复用问题
计算机界有一句名言:“计算机科学领域的任何问题都可以通过增加一个中间层来解决。”(原始版本出自计算机科学家David Wheeler)相似的,为了视图层的复用,屏蔽数据层的差异,我们在数据层的逻辑中转部分引入一个中间层ViewData,ViewData是为了更好地适配数据模型以及区别视图展示上的差异,这样就大大提高了代码的复用率。
收益
解耦的收益
开发效率提升
控制器瘦身
包体积减少
动态化的收益
两端对齐的收益
总结与展望
附录
1. Mach (马赫) 是外卖终端组自研的多终端跨平台级的局部动态化技术。
2. MRN 是美团基于React-native 0.54.3进行的二次封装,抹平了两端上的差异,并且提供了一些基础库和组件库供业务开发同学使用。
3. Metrics 是美团平台团队和外卖团队,开发的新一代App性能采集、监控、统计平台。
4. Hertz(赫兹)是一个自动化的性能采集与监控SDK,可以在开发、测试、灰度、运维各阶段,采集性能指标、检测卡顿、测量页面加载时间,帮助开发者监控和定位性能问题。
作者简介
---------- END ----------
招聘信息
美团外卖长期招聘 Android、iOS、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到tech@meituan.com。